/*
 * Copyright (C) 2011 Andreas Petersson
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package at.petersson.interner;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.*;

import java.awt.*;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * @author apetersson
 */
public class ImmutabilityChecker {

    private static final Predicate<Field> NON_STATIC = new Predicate<Field>() {
        @Override
        public boolean apply(Field input) {
            return !Modifier.isStatic(input.getModifiers());
        }
    };
    private static final Function<Class<?>, Class<?>> GET_SUPERCLASS = new Function<Class<?>, Class<?>>() {
        @Override
        public Class<?> apply(Class<?> from) {
            return from.getSuperclass();
        }
    };
    private static final Function<Class<?>, Iterable<Field>> CLASS_2_FIELDS = new Function<Class<?>, Iterable<Field>>() {
        @Override
        public Iterable<Field> apply(Class<?> from) {
            return Iterables.concat(Arrays.asList(from.getFields()), Arrays.asList(from.getDeclaredFields()));
        }
    };

    //cheap to check, add as many as you want
    private final Set<Class<?>> knownImmutables = Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>());
    //more expensive to check, hold back!
    private final Set<Class<?>> knownAbstractImmutables = Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>());
    private final ImmutableSet<Class<? extends Annotation>> immutableAnnotations;

    public ImmutabilityChecker(Class<? extends Annotation>... immutableAnnotations) {
        this.immutableAnnotations = ImmutableSet.copyOf(immutableAnnotations);
        knownImmutables.add(Pattern.class);
        knownImmutables.add(String.class);
        knownImmutables.add(Locale.class);
        knownImmutables.add(SimpleTimeZone.class);
        knownImmutables.add(StackTraceElement.class);
        knownImmutables.add(BigInteger.class);
        knownImmutables.add(BigDecimal.class);
        knownImmutables.add(Font.class);
        knownImmutables.add(BasicStroke.class);
        knownImmutables.add(Cursor.class);
        knownImmutables.add(UUID.class);
        knownImmutables.add(Inet4Address.class);
        knownImmutables.add(Inet6Address.class);
        knownImmutables.add(File.class);
        knownAbstractImmutables.add(Color.class);
        knownAbstractImmutables.add(Paint.class);
        knownAbstractImmutables.add(ImmutableCollection.class);
        knownAbstractImmutables.add(ImmutableMap.class);
        knownAbstractImmutables.add(ImmutableMultimap.class);
    }

    /*
         this is not part of the public api! do not use.
          */
    String isImmutable(Class<?> aClass) {
        return isImmutable(aClass, new HashSet<Class<?>>(), true);
    }

    private String isImmutable(Class<?> aClass, Set<Class<?>> alreadyUnderInspection, boolean considerFinal) {
        //fast track: we already checked it.
        if (knownImmutables.contains(aClass)) {
            return null;
        }
        if (alreadyUnderInspection.contains(aClass)) { //this will be accounted for later..
            return null;
        }
        alreadyUnderInspection.add(aClass);
        if (checkForPositiveMarkers(aClass)) return null;
        if (checkForArrays(aClass)) return aClass.getCanonicalName() + "[isArray]";
        if (checkForInterfaces(aClass)) return aClass.getCanonicalName() + "[isInterface]";
        if (checkForAbstract(aClass)) return aClass.getCanonicalName() + "[isAbstract]";
        if (checkForNonFinal(aClass, considerFinal)) return aClass.getCanonicalName() + "[classNotFinal]";
        //recurse, do this check for all
        for (Field field : nonstaticFields(aClass)) {
            String errorMessage = fieldBreaksImmutability(field, alreadyUnderInspection);
            if (errorMessage != null) {
                return field.getDeclaringClass().getCanonicalName() + "{" + field.getName() + "}" + errorMessage;
            }
        }
        //success! we found an immutable class
        knownImmutables.add(aClass);
        return null;
    }

    private boolean checkForPositiveMarkers(Class<?> aClass) {
        boolean foundMarker = checkForAnnotations(aClass) || checkForPrimitives(aClass) || checkForSuperclasses(aClass);
        if (foundMarker) {
            knownImmutables.add(aClass);
        }
        return foundMarker;
    }

    private boolean checkForNonFinal(Class<?> aClass, boolean considerFinal) {
        //can be any implementation, so might be mutable
        return !considerFinal && !Modifier.isFinal(aClass.getModifiers());
    }

    private boolean checkForAbstract(Class<?> aClass) {
        //can be any implementation, so might be mutable
        return Modifier.isAbstract(aClass.getModifiers());
    }

    private boolean checkForInterfaces(Class<?> aClass) {
        //can be any implementation, so might be mutable
        return Modifier.isInterface(aClass.getModifiers());
    }

    private boolean checkForArrays(Class<?> aClass) {
        //arrays are always mutable, unless we know there is no access to them, itc add them to knownImmutables
        return aClass.isArray();
    }

    private boolean checkForSuperclasses(Class<?> aClass) {
        for (Class<?> immutable : knownAbstractImmutables) {
            if (immutable.isAssignableFrom(aClass)) {
                return true;
            }
        }
        return false;
    }

    private boolean checkForPrimitives(Class<?> aClass) {
        //enums might be mutable, but for our purpose, hashCode() and equals() will not change, even if its fields change..
        //so we can treat them as immutable
        return aClass.isPrimitive() || aClass.isEnum();
    }

    private boolean checkForAnnotations(Class<?> aClass) {
        for (Annotation annotation : aClass.getAnnotations()) {
            if (immutableAnnotations.contains(annotation.annotationType())) {
                return true;
            }
        }
        return false;
    }

    private static <T> Iterable<T> recursive(final T start, final Function<T, T> nextElement) {
        return new Iterable<T>() {
            @Override
            public Iterator<T> iterator() {
                return new AbstractIterator<T>() {
                    private T current = start;

                    @Override
                    protected T computeNext() {
                        if (current == null) return endOfData();
                        try {
                            return current;
                        } finally {
                            current = nextElement.apply(current);
                        }
                    }
                };
            }
        };
    }

    //move to util class?
    static Iterable<Field> nonstaticFields(final Class<?> someClass) {
        Iterable<Class<?>> allClasses = recursive(someClass, GET_SUPERCLASS);
        Iterable<Iterable<Field>> allFields = Iterables.transform(allClasses, CLASS_2_FIELDS);
        return Iterables.filter(Iterables.concat(allFields), NON_STATIC);
    }

    private String fieldBreaksImmutability(Field field, Set<Class<?>> alreadyUnderInspection) {
        if (!Modifier.isFinal(field.getModifiers())) {
            return "[notFinal]";
        }
        return isImmutable(field.getType(), alreadyUnderInspection, false);
    }
}
